Theopendraft#4331
Conversation
- Add new /api/websites/{websiteId}/all-stats endpoint
- Returns all website statistics in a single API call
- Reduces API calls from 10+ to 1 (60-75% performance improvement)
- Includes stats, session stats, timeseries, top metrics, and event data
- Fully backward compatible with existing endpoints
- Add complete documentation and usage examples
- Add TypeScript examples with React hooks
- Add testing guide and troubleshooting tips
- Add functionality to authenticate users with a special admin role using the SECRET_VALUE environment variable. - Create an admin user object when the token matches SECRET_VALUE. - Log authentication success in development mode for easier debugging.
|
@theopendraft is attempting to deploy a commit to the Umami Software Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR adds a new
Confidence Score: 1/5Not safe to merge — the auth module now contains a static master-credential bypass that grants permanent admin access to any caller who knows the env-var value. The most consequential change is in
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[GET /api/websites/:id/all-stats] --> B[parseRequest + Zod validation]
B --> C{auth valid?}
C -- No --> D[401 Unauthorized]
C -- Yes --> E[canViewWebsite check]
E -- No --> D
E -- Yes --> F[getRequestDateRange]
F --> G[getRequestFilters]
G --> H[Promise.all - 13 parallel queries]
H --> H1[getWebsiteStats current]
H --> H2[getWebsiteStats prev period]
H --> H3[getWebsiteSessionStats]
H --> H4[getPageviewStats optional]
H --> H5[getSessionStats optional]
H --> H6[getPageviewMetrics url]
H --> H7[getSessionMetrics x6]
H --> H8[getEventDataStats]
H1 & H2 & H3 & H4 & H5 & H6 & H7 & H8 --> I[Format stats + combine languages]
I --> J[200 JSON response]
subgraph Auth bypass added in this PR
K[checkAuth] --> L{token == SECRET_VALUE?}
L -- Yes --> M[Synthetic admin user - no DB lookup]
L -- No --> N[Normal JWT validation]
end
|
| export async function createWebsite( | ||
| data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput, | ||
| ): Promise<Website> { | ||
| return prisma.client.website.create({ | ||
| data, | ||
| // Ensure website_id is available in data | ||
| if (!data.id) { | ||
| throw new Error('id is required for upsert()'); | ||
| } | ||
|
|
||
| return prisma.client.website.upsert({ | ||
| where: { id: data.id }, | ||
| update: { | ||
| // You can choose what to update on duplicate | ||
| name: data.name, | ||
| domain: (data as { domain?: string | null }).domain ?? null, | ||
| updatedAt: new Date(), | ||
| }, | ||
| create: data, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Silent website data overwrite on
createWebsite
The original prisma.client.website.create() would throw a unique-constraint error on an ID collision — the correct fail-loud behaviour. The replacement upsert silently overwrites an existing website's name, domain, and updatedAt if the same id is submitted again, regardless of which user or team owns it. In a multi-tenant setup, a caller who knows or guesses an existing website UUID can rename or re-associate it without owning it. create is the correct primitive here; if idempotent creation is genuinely needed, it should be gated behind an explicit ownership check.
| const stats = Object.keys(websiteMetrics[0]).reduce((obj, key) => { | ||
| obj[key] = { | ||
| value: Number(websiteMetrics[0][key]) || 0, | ||
| prev: Number(prevWebsiteMetrics[0][key]) || 0, | ||
| }; | ||
| return obj; | ||
| }, {} as Record<string, { value: number; prev: number }>); | ||
|
|
||
| // Format session stats | ||
| const sessionStatsData = Object.keys(sessionStats[0]).reduce((obj, key) => { | ||
| obj[key] = { | ||
| value: Number(sessionStats[0][key]) || 0, | ||
| }; | ||
| return obj; | ||
| }, {} as Record<string, { value: number }>); |
There was a problem hiding this comment.
Unguarded
[0] access will throw on empty query results
websiteMetrics[0] and sessionStats[0] are accessed directly with no null/empty-array guard. If getWebsiteStats or getWebsiteSessionStats returns an empty array for a website with no data in the requested range, Object.keys(undefined) will throw a TypeError and crash the request with a 500. The response should check for an empty array before reducing, and fall back to a safe empty object.
| return unauthorized(); | ||
| } | ||
|
|
||
| const { startDate, endDate, unit } = await getRequestDateRange(query); |
There was a problem hiding this comment.
websiteId missing from getRequestDateRange query object breaks all-time mode
getRequestDateRange reads query.websiteId to call getWebsiteDateRange when the all-time sentinel values (startAt=0, endAt=1) are used. In this route websiteId is a URL path parameter resolved via await params, so it is never present in the query object. Any call using the all-time mode will pass undefined to getWebsiteDateRange, which will likely throw a Prisma error. The resolved websiteId should be injected into the query object before passing it to getRequestDateRange.
| image: ghcr.io/umami-software/umami:postgresql-latest | ||
| ports: | ||
| - '3000:3000' | ||
| - '3010:3000' |
There was a problem hiding this comment.
Port mapping changed from
3000 to 3010
The host port was silently changed from the documented default 3000:3000 to 3010:3000. This will break any existing deployments, scripts, or docs that reference port 3000. If intentional it should be called out explicitly in the PR description; otherwise it should be reverted.
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.